package org.tlang.ast.list.func;

import org.tlang.ast.AST;
import org.tlang.ast.ASTLeaf;
import org.tlang.ast.ASTList;
import org.tlang.context.Location;
import org.tlang.context.SymbolTable;
import org.tlang.context.TypeTable;
import org.tlang.context.ValueTable;
import org.tlang.exception.EvalException;
import org.tlang.exception.TypeException;
import org.tlang.metaclass.TFunction;
import org.tlang.type.FuncType;
import org.tlang.type.Type;

import java.util.List;

public class FunctionInvoker extends ASTList {
    private int nest;
    private int index;

    public FunctionInvoker(List<AST> children) {
        super(children);
        this.index = Location.UNKNOWN;
    }

    public String name() {
        return ((ASTLeaf) child(0)).token().getText();
    }

    public ArgumentList arguments() {
        return (ArgumentList) child(1);
    }

    @Override
    public void lookup(SymbolTable symbolTable, TypeTable typeTable) {
        lookupName(symbolTable);
        arguments().lookup(symbolTable, typeTable);
    }

    public void lookupAsMethodInvoker(SymbolTable callerSymbolTable, TypeTable callerTypeTable,
                                      SymbolTable objSymbolTable, TypeTable objTypeTable) {
        lookupName(objSymbolTable);
        arguments().lookup(callerSymbolTable, callerTypeTable);
    }

    private void lookupName(SymbolTable symbolTable) {
        Location location = symbolTable.where(name());
        if (location == null) {
            throw new EvalException("undefined identifier: " + this, this);
        }

        nest = location.nest();
        index = location.index();
    }

    @Override
    public Type typeCheck(TypeTable typeTable) throws TypeException {
        FuncType funcType = (FuncType) typeTable.get(nest, index);
        arguments().typeCheck(typeTable);
        if (!funcType.matchParameterTypes(arguments().types())) {
            throw new TypeException("arguments not match parameters type", this);
        }
        return funcType.returnType();
    }

    @Override
    public Object eval(ValueTable valueTable) {
        Object function = valueTable.get(nest, index);
        return execute(valueTable, function);
    }

    public Object evalAsMethodInvoker(ValueTable callerValueTable, ValueTable objValueTable) {
        Object method = objValueTable.get(nest, index);
        return execute(callerValueTable, method);
    }

    private Object execute(ValueTable valueTable, Object function) {
        if (function instanceof TFunction) {
            return ((TFunction) function).execute(valueTable, arguments());
        }

        throw new EvalException("function \"" + name() + "\" is not defined", this);
    }
}
